diff --git a/examples/README.md b/examples/README.md index b35337c0b..a69801d31 100644 --- a/examples/README.md +++ b/examples/README.md @@ -135,6 +135,7 @@ Create a multi-agent research workflow where different AI agents collaborate to - [Sub‑agents](./with-subagents) — Supervisor orchestrates focused sub‑agents to divide tasks. - [Supabase](./with-supabase) — Use Supabase auth/database in tools and server endpoints. - [Tavily Search](./with-tavily-search) — Augment answers with web results from Tavily. +- [Xquik Tools](./with-xquik-tools) — Research public X/Twitter posts, users, and trends with Xquik tools. - [Thinking Tool](./with-thinking-tool) — Structured reasoning via a dedicated “thinking” tool and schema. - [Tool Routing](./with-tool-routing) — Route large tool pools through a small set of router tools. - [Tools](./with-tools) — Author Zod‑typed tools with cancellation and streaming support. diff --git a/examples/with-xquik-tools/.env.example b/examples/with-xquik-tools/.env.example new file mode 100644 index 000000000..8ae460b06 --- /dev/null +++ b/examples/with-xquik-tools/.env.example @@ -0,0 +1,3 @@ +OPENAI_API_KEY=your_openai_api_key_here +XQUIK_API_KEY=your_xquik_api_key_here +XQUIK_BASE_URL=https://xquik.com/api/v1 diff --git a/examples/with-xquik-tools/README.md b/examples/with-xquik-tools/README.md new file mode 100644 index 000000000..b333c5fb4 --- /dev/null +++ b/examples/with-xquik-tools/README.md @@ -0,0 +1,107 @@ +# VoltAgent with Xquik Tools + +This example shows how to add Xquik REST API tools to a VoltAgent research agent for public X/Twitter data. + +## Features + +- Search public posts with X query operators +- Look up posts by ID with author, metrics, and media +- Look up public user profiles by username or ID +- Fetch recent public posts from a user +- Retrieve public X/Twitter trends by WOEID region + +## Prerequisites + +1. **Xquik API Key**: Create an API key from the [Xquik dashboard](https://dashboard.xquik.com) +2. **OpenAI API Key**: Used by the example agent model + +## Setup + +1. Install dependencies: + + ```bash + pnpm install + ``` + +2. Configure environment variables: + + ```bash + cp .env.example .env + ``` + + Then edit `.env`: + + ```env + OPENAI_API_KEY=your_openai_api_key_here + XQUIK_API_KEY=your_xquik_api_key_here + XQUIK_BASE_URL=https://xquik.com/api/v1 + ``` + +3. Run the example: + + ```bash + pnpm dev + ``` + +The agent runs on the default VoltAgent server port and exposes one agent named `xquikResearchAgent`. + +## Tools Available + +### 1. Search X Posts + +- **Purpose**: Search public X/Twitter posts with X query operators +- **Endpoint**: `GET /x/tweets/search` +- **Parameters**: + - `query`: Search query string + - `queryType`: `Latest` or `Top` + - `limit`: Maximum posts to return + - `cursor`: Optional pagination cursor + - `sinceTime`, `untilTime`: Optional ISO 8601 time bounds + +### 2. Get X Post + +- **Purpose**: Look up a public post by ID +- **Endpoint**: `GET /x/tweets/{id}` + +### 3. Get X User + +- **Purpose**: Look up a public user profile by username or ID +- **Endpoint**: `GET /x/users/{id}` + +### 4. Get X User Posts + +- **Purpose**: Fetch recent public posts from a user +- **Endpoint**: `GET /x/users/{id}/tweets` +- **Parameters**: + - `user`: Username without `@`, or numeric user ID + - `cursor`: Optional pagination cursor + - `includeReplies`: Include replies + - `includeParentTweet`: Include parent posts for replies + +### 5. Get X Trends + +- **Purpose**: Fetch public trends by WOEID region +- **Endpoint**: `GET /x/trends` +- **Parameters**: + - `woeid`: Region WOEID, with `1` for worldwide + - `count`: Number of trends to return + +## Example Queries + +- "Search recent posts about AI agent frameworks and summarize the top themes." +- "Look up @voltagent_dev and summarize recent public posts." +- "Find worldwide X trends and explain which ones relate to developer tools." +- "Get this post by ID and extract the author, timestamp, and engagement metrics." + +## API Integration + +This example uses the Xquik public REST API with the `x-api-key` header and the `xquik-api-contract` header set to `2026-04-29`. + +See the [Xquik API reference](https://docs.xquik.com/api-reference/overview) for endpoint details. + +## Troubleshooting + +- **Missing API key**: Set `XQUIK_API_KEY` in `.env`. +- **Authentication errors**: Create a fresh API key from the Xquik dashboard. +- **Empty results**: Try a broader query, switch between `Latest` and `Top`, or remove time bounds. +- **Regional trends**: Use WOEID `1` for worldwide, `23424977` for the US, or another supported region. diff --git a/examples/with-xquik-tools/package.json b/examples/with-xquik-tools/package.json new file mode 100644 index 000000000..0172cac47 --- /dev/null +++ b/examples/with-xquik-tools/package.json @@ -0,0 +1,38 @@ +{ + "name": "voltagent-example-with-xquik-tools", + "author": "", + "dependencies": { + "@voltagent/cli": "^0.1.21", + "@voltagent/core": "^2.7.4", + "@voltagent/logger": "^2.0.2", + "@voltagent/server-hono": "^2.0.13", + "ai": "^6.0.0", + "zod": "^3.25.76" + }, + "devDependencies": { + "@types/node": "^24.2.1", + "tsx": "^4.21.0", + "typescript": "^5.8.2" + }, + "keywords": [ + "agent", + "ai", + "twitter", + "voltagent", + "xquik" + ], + "license": "MIT", + "private": true, + "repository": { + "type": "git", + "url": "https://github.com/VoltAgent/voltagent.git", + "directory": "examples/with-xquik-tools" + }, + "scripts": { + "build": "tsc", + "dev": "tsx watch --env-file=.env ./src", + "start": "node dist/index.js", + "volt": "volt" + }, + "type": "module" +} diff --git a/examples/with-xquik-tools/src/index.ts b/examples/with-xquik-tools/src/index.ts new file mode 100644 index 000000000..11fd22084 --- /dev/null +++ b/examples/with-xquik-tools/src/index.ts @@ -0,0 +1,28 @@ +import { Agent, VoltAgent } from "@voltagent/core"; +import { createPinoLogger } from "@voltagent/logger"; +import { honoServer } from "@voltagent/server-hono"; +import { xquikTools } from "./tools"; + +const logger = createPinoLogger({ + name: "xquik-tools-agent", + level: "info", +}); + +const xquikResearchAgent = new Agent({ + name: "xquik-research-agent", + instructions: `You help developers research public X/Twitter activity with Xquik tools. + +Use the tools for post search, post lookup, user lookup, user posts, and trends. +Prefer concise answers with source IDs, usernames, timestamps, and relevant metrics. +If a tool reports a missing API key or HTTP error, explain the setup or retry path clearly.`, + model: "openai/gpt-4o-mini", + tools: xquikTools, +}); + +new VoltAgent({ + agents: { + xquikResearchAgent, + }, + logger, + server: honoServer(), +}); diff --git a/examples/with-xquik-tools/src/tools.ts b/examples/with-xquik-tools/src/tools.ts new file mode 100644 index 000000000..cc4b2ff91 --- /dev/null +++ b/examples/with-xquik-tools/src/tools.ts @@ -0,0 +1,194 @@ +import { createTool } from "@voltagent/core"; +import { z } from "zod"; + +const XQUIK_API_CONTRACT = "2026-04-29"; +const DEFAULT_XQUIK_BASE_URL = "https://xquik.com/api/v1"; +const XQUIK_REQUEST_TIMEOUT_MS = 20_000; + +type XquikQueryParams = Record; + +type XquikResult = { + data?: unknown; + error?: string; + status?: number; + success: boolean; +}; + +const queryTypeSchema = z.enum(["Latest", "Top"]); + +/** + * Returns the configured Xquik API base URL without a trailing slash. + */ +function getXquikBaseUrl(): string { + return (process.env.XQUIK_BASE_URL ?? DEFAULT_XQUIK_BASE_URL).replace(/\/+$/, ""); +} + +/** + * Normalizes an X username or user ID for use in URL path segments. + */ +function encodeXIdentifier(value: string): string { + const identifier = value.trim().replace(/^@+/, ""); + if (!identifier) { + throw new Error("A username or user ID is required."); + } + return encodeURIComponent(identifier); +} + +/** + * Adds defined query parameters to a request URL. + */ +function appendQueryParams(url: URL, params: XquikQueryParams): void { + for (const [key, value] of Object.entries(params)) { + if (value === undefined || value === "") { + continue; + } + + url.searchParams.set(key, typeof value === "boolean" ? String(value) : String(value)); + } +} + +/** + * Calls a read-only Xquik REST endpoint and returns a tool-friendly result. + */ +async function callXquik(path: string, params: XquikQueryParams = {}): Promise { + const apiKey = process.env.XQUIK_API_KEY; + if (!apiKey) { + return { + success: false, + error: "XQUIK_API_KEY is not set. Add it to .env before calling live Xquik tools.", + }; + } + + const url = new URL(`${getXquikBaseUrl()}${path}`); + appendQueryParams(url, params); + + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), XQUIK_REQUEST_TIMEOUT_MS); + + try { + const response = await fetch(url, { + headers: { + "x-api-key": apiKey, + "xquik-api-contract": XQUIK_API_CONTRACT, + }, + method: "GET", + signal: controller.signal, + }); + + if (!response.ok) { + const body = (await response.text()).slice(0, 500); + return { + success: false, + status: response.status, + error: `Xquik API returned HTTP ${response.status}: ${body || response.statusText}`, + }; + } + + return { + success: true, + data: await response.json(), + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : "Xquik request failed.", + }; + } finally { + clearTimeout(timeout); + } +} + +/** + * Searches public X/Twitter posts with Xquik query operators. + */ +export const searchXPostsTool = createTool({ + name: "searchXPosts", + description: + "Search recent public X/Twitter posts with X query operators, chronological or engagement-ranked sorting, and pagination.", + parameters: z.object({ + query: z + .string() + .min(1) + .describe('Search query, such as "agent frameworks", "from:voltagent_dev", or "#AI".'), + queryType: queryTypeSchema.optional().describe('Sort order. Use "Latest" or "Top".'), + limit: z.number().int().min(1).max(50).optional().describe("Maximum posts to return."), + cursor: z.string().optional().describe("Pagination cursor from a previous response."), + sinceTime: z.string().optional().describe("ISO 8601 timestamp to search after."), + untilTime: z.string().optional().describe("ISO 8601 timestamp to search before."), + }), + execute: async ({ query, queryType = "Latest", limit = 10, cursor, sinceTime, untilTime }) => + callXquik("/x/tweets/search", { + q: query, + queryType, + limit, + cursor, + sinceTime, + untilTime, + }), +}); + +/** + * Looks up a public X/Twitter post by ID. + */ +export const getXPostTool = createTool({ + name: "getXPost", + description: + "Look up a public X/Twitter post by ID and return its text, author, metrics, and media.", + parameters: z.object({ + postId: z.string().min(1).describe("Numeric X/Twitter post ID."), + }), + execute: async ({ postId }) => callXquik(`/x/tweets/${encodeURIComponent(postId.trim())}`), +}); + +/** + * Looks up a public X/Twitter user profile by username or ID. + */ +export const getXUserTool = createTool({ + name: "getXUser", + description: "Look up a public X/Twitter user profile by username or user ID.", + parameters: z.object({ + user: z.string().min(1).describe("X username without @, or a numeric X user ID."), + }), + execute: async ({ user }) => callXquik(`/x/users/${encodeXIdentifier(user)}`), +}); + +/** + * Fetches recent public posts for an X/Twitter user. + */ +export const getXUserPostsTool = createTool({ + name: "getXUserPosts", + description: "Fetch recent public posts from an X/Twitter user by username or user ID.", + parameters: z.object({ + user: z.string().min(1).describe("X username without @, or a numeric X user ID."), + cursor: z.string().optional().describe("Pagination cursor from a previous response."), + includeReplies: z.boolean().optional().describe("Include replies in the returned posts."), + includeParentTweet: z.boolean().optional().describe("Include parent posts for replies."), + }), + execute: async ({ user, cursor, includeReplies = false, includeParentTweet = false }) => + callXquik(`/x/users/${encodeXIdentifier(user)}/tweets`, { + cursor, + includeReplies, + includeParentTweet, + }), +}); + +/** + * Fetches public X/Twitter trends for a WOEID region. + */ +export const getXTrendsTool = createTool({ + name: "getXTrends", + description: "Fetch public X/Twitter trending topics by WOEID region.", + parameters: z.object({ + woeid: z.number().int().min(1).optional().describe("WOEID region. Use 1 for worldwide."), + count: z.number().int().min(1).max(50).optional().describe("Number of trends to return."), + }), + execute: async ({ woeid = 1, count = 10 }) => callXquik("/x/trends", { woeid, count }), +}); + +export const xquikTools = [ + searchXPostsTool, + getXPostTool, + getXUserTool, + getXUserPostsTool, + getXTrendsTool, +]; diff --git a/examples/with-xquik-tools/tsconfig.json b/examples/with-xquik-tools/tsconfig.json new file mode 100644 index 000000000..6561a6910 --- /dev/null +++ b/examples/with-xquik-tools/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "outDir": "dist", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "skipLibCheck": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c98a38cc1..1812184f7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3566,6 +3566,37 @@ importers: specifier: ^5.8.2 version: 5.9.3 + examples/with-xquik-tools: + dependencies: + '@voltagent/cli': + specifier: ^0.1.21 + version: link:../../packages/cli + '@voltagent/core': + specifier: ^2.7.4 + version: link:../../packages/core + '@voltagent/logger': + specifier: ^2.0.2 + version: link:../../packages/logger + '@voltagent/server-hono': + specifier: ^2.0.13 + version: link:../../packages/server-hono + ai: + specifier: ^6.0.0 + version: 6.0.3(zod@3.25.76) + zod: + specifier: ^3.25.76 + version: 3.25.76 + devDependencies: + '@types/node': + specifier: ^24.2.1 + version: 24.6.2 + tsx: + specifier: ^4.21.0 + version: 4.21.0 + typescript: + specifier: ^5.8.2 + version: 5.9.3 + examples/with-youtube-to-blog: dependencies: '@voltagent/cli':