Skip to content
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
33 changes: 30 additions & 3 deletions src/lib/agents/search/researcher/actions/search/socialSearch.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import z from 'zod';
import { ResearchAction } from '../../../types';
import { ResearchBlock } from '@/lib/types';
import { ResearchBlock, Chunk } from '@/lib/types';
import { executeSearch } from './baseSearch';
import { isXquikEnabled, searchXquik } from '@/lib/xquik';

const schema = z.object({
queries: z.array(z.string()).describe('List of social search queries'),
Expand All @@ -10,6 +11,7 @@ const schema = z.object({
const socialSearchDescription = `
Use this tool to perform social media searches for relevant posts, discussions, and trends related to the user's query. Provide a list of concise search queries that will help gather comprehensive social media information on the topic at hand.
You can provide up to 3 queries at a time. Make sure the queries are specific and relevant to the user's needs.
${isXquikEnabled ? 'This tool searches both Reddit and X/Twitter for broader social media coverage.' : 'This tool searches Reddit for social media discussions.'}

For example, if the user is interested in public opinion on electric vehicles, your queries could be:
1. "Electric vehicles public opinion 2024"
Expand Down Expand Up @@ -40,7 +42,8 @@ const socialSearchAction: ResearchAction<typeof schema> = {

if (!researchBlock) throw new Error('Failed to retrieve research block');

const results = await executeSearch({
// Search Reddit via SearXNG
const redditResults = await executeSearch({
llm: additionalConfig.llm,
embedding: additionalConfig.embedding,
mode: additionalConfig.mode,
Expand All @@ -52,9 +55,33 @@ const socialSearchAction: ResearchAction<typeof schema> = {
},
});

// Search X/Twitter via Xquik (if configured)
let xResults: Chunk[] = [];
if (isXquikEnabled) {
try {
const xquikResults = await Promise.all(
input.queries.map((q) => searchXquik(q, 5)),
);

xResults = xquikResults.flatMap((res) =>
res.results.map((r) => ({
content: r.content,
metadata: {
title: r.title,
url: r.url,
similarity: 1,
embedding: [],
},
})),
);
} catch (err) {
console.error('Xquik social search error:', err);
}
}

return {
type: 'search_results',
results: results,
results: [...redditResults, ...xResults],
};
},
};
Expand Down
87 changes: 87 additions & 0 deletions src/lib/xquik.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/**
* Xquik X/Twitter search provider.
* Searches tweets via the Xquik REST API when XQUIK_API_KEY is set.
* Returns results in the same shape as SearXNG for seamless integration.
*/

const XQUIK_API_KEY = process.env.XQUIK_API_KEY;

export const isXquikEnabled = !!XQUIK_API_KEY;

interface XquikSearchResult {
title: string;
url: string;
content: string;
}

export const searchXquik = async (
query: string,
limit: number = 10,
): Promise<{ results: XquikSearchResult[] }> => {
if (!XQUIK_API_KEY) {
return { results: [] };
}

const params = new URLSearchParams({
q: query,
limit: String(Math.min(limit, 50)),
queryType: 'Top',
});

const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);

try {
const res = await fetch(
`https://xquik.com/api/v1/x/tweets/search?${params}`,
{
headers: {
'X-API-Key': XQUIK_API_KEY,
Accept: 'application/json',
},
signal: controller.signal,
},
);

if (!res.ok) {
console.error(`Xquik search error: ${res.status}`);
return { results: [] };
}

const data = await res.json();
const tweets = data.tweets || [];

const results: XquikSearchResult[] = tweets.map((tweet: any) => {
const author = tweet.author || {};
const username = author.username || 'unknown';
const likes = tweet.likeCount || 0;
const retweets = tweet.retweetCount || 0;
const views = tweet.viewCount || 0;

const engagement = [
likes > 0 ? `${likes} likes` : '',
retweets > 0 ? `${retweets} retweets` : '',
views > 0 ? `${views} views` : '',
]
.filter(Boolean)
.join(', ');

return {
title: `@${username}: ${(tweet.text || '').slice(0, 120)}`,
url: `https://x.com/${username}/status/${tweet.id}`,
content: `${tweet.text || ''}${engagement ? ` [${engagement}]` : ''}`,
};
});

return { results };
} catch (err: any) {
if (err.name === 'AbortError') {
console.error('Xquik search timed out');
} else {
console.error(`Xquik search error: ${err.message}`);
}
return { results: [] };
} finally {
clearTimeout(timeoutId);
}
};