Skip to content
Merged
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
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions examples/with-xquik-tools/.env.example
Original file line number Diff line number Diff line change
@@ -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
107 changes: 107 additions & 0 deletions examples/with-xquik-tools/README.md
Original file line number Diff line number Diff line change
@@ -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.
38 changes: 38 additions & 0 deletions examples/with-xquik-tools/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
28 changes: 28 additions & 0 deletions examples/with-xquik-tools/src/index.ts
Original file line number Diff line number Diff line change
@@ -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(),
});
194 changes: 194 additions & 0 deletions examples/with-xquik-tools/src/tools.ts
Original file line number Diff line number Diff line change
@@ -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<string, boolean | number | string | undefined>;

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<XquikResult> {
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,
];
16 changes: 16 additions & 0 deletions examples/with-xquik-tools/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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/**/*"]
}
Loading
Loading