Skip to content

Commit 27abde0

Browse files
authored
Merge pull request #1734 from kriptoburak/feat/xquik-retriever
feat: add Xquik X/Twitter search retriever
2 parents 645f24c + 4d87f15 commit 27abde0

6 files changed

Lines changed: 104 additions & 1 deletion

File tree

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
OPENAI_API_KEY=
22
TAVILY_API_KEY=
3+
XQUIK_API_KEY=
34
DOC_PATH=./my-docs
45

56
# NEXT_PUBLIC_GPTR_API_URL=http://0.0.0.0:8000 # Defaults to localhost:8000 if not set

gpt_researcher/actions/retriever.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ def get_retriever(retriever: str):
2929
- pubmed_central: PubMed Central medical literature
3030
- custom: Custom user-defined retriever
3131
- mcp: Model Context Protocol retriever
32+
- xquik: Xquik X/Twitter search
3233
"""
3334
match retriever:
3435
case "google":
@@ -91,6 +92,10 @@ def get_retriever(retriever: str):
9192
from gpt_researcher.retrievers import MCPRetriever
9293

9394
return MCPRetriever
95+
case "xquik":
96+
from gpt_researcher.retrievers import XquikSearch
97+
98+
return XquikSearch
9499

95100
case _:
96101
return None

gpt_researcher/retrievers/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from .exa.exa import ExaSearch
1414
from .mcp import MCPRetriever
1515
from .bocha.bocha import BoChaSearch
16+
from .xquik.xquik import XquikSearch
1617

1718
__all__ = [
1819
"TavilySearch",
@@ -29,5 +30,6 @@
2930
"PubMedCentralSearch",
3031
"ExaSearch",
3132
"MCPRetriever",
32-
"BoChaSearch"
33+
"BoChaSearch",
34+
"XquikSearch"
3335
]

gpt_researcher/retrievers/utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ def check_pkg(pkg: str) -> None:
7474
"pubmed_central",
7575
"exa",
7676
"mcp",
77+
"xquik",
7778
"mock"
7879
]
7980

gpt_researcher/retrievers/xquik/__init__.py

Whitespace-only changes.
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# Xquik X/Twitter Retriever
2+
#
3+
# Searches X (Twitter) for real-time perspectives, dev discussions,
4+
# product feedback, breaking news, and expert opinions.
5+
# $0.00015 per tweet — 33x cheaper than the official X API.
6+
7+
import json
8+
import os
9+
import urllib.parse
10+
import urllib.request
11+
12+
13+
class XquikSearch:
14+
"""
15+
Xquik X/Twitter search retriever.
16+
17+
Searches tweets via the Xquik REST API and returns results in the
18+
standard {title, href, body} format used by all GPT Researcher retrievers.
19+
20+
Set XQUIK_API_KEY in your environment. Get one at https://xquik.com
21+
"""
22+
23+
def __init__(self, query, query_domains=None, **kwargs):
24+
self.query = query
25+
self.query_domains = query_domains
26+
self.api_key = self.get_api_key()
27+
28+
def get_api_key(self):
29+
try:
30+
api_key = os.environ["XQUIK_API_KEY"]
31+
except KeyError:
32+
raise Exception(
33+
"Xquik API key not found. Please set the XQUIK_API_KEY "
34+
"environment variable. Get a key at https://xquik.com"
35+
)
36+
return api_key
37+
38+
def search(self, max_results=10):
39+
"""
40+
Search X/Twitter via Xquik API.
41+
42+
Returns:
43+
list: Search results as [{title, href, body}, ...]
44+
"""
45+
print(f"Searching X/Twitter with query: {self.query}...")
46+
47+
try:
48+
results = self._search_tweets(max_results)
49+
return results
50+
except Exception as e:
51+
print(f"Error: {e}. Failed fetching X/Twitter sources. Resulting in empty response.")
52+
return []
53+
54+
def _search_tweets(self, max_results):
55+
params = urllib.parse.urlencode({
56+
"q": self.query,
57+
"limit": min(max_results, 200),
58+
"queryType": "Top",
59+
})
60+
url = f"https://xquik.com/api/v1/x/tweets/search?{params}"
61+
62+
req = urllib.request.Request(url, headers={
63+
"X-API-Key": self.api_key,
64+
"Accept": "application/json",
65+
"User-Agent": "gpt-researcher/1.0",
66+
})
67+
68+
with urllib.request.urlopen(req, timeout=15) as resp:
69+
data = json.loads(resp.read().decode("utf-8"))
70+
71+
tweets = data.get("tweets", [])
72+
search_results = []
73+
74+
for tweet in tweets[:max_results]:
75+
author = tweet.get("author", {})
76+
username = author.get("username", "unknown")
77+
text = tweet.get("text", "")
78+
tweet_id = tweet.get("id", "")
79+
80+
likes = tweet.get("likeCount", 0)
81+
retweets = tweet.get("retweetCount", 0)
82+
views = tweet.get("viewCount", 0)
83+
84+
engagement = f"{likes} likes, {retweets} RTs"
85+
if views:
86+
engagement += f", {views} views"
87+
88+
search_results.append({
89+
"title": f"@{username}: {text[:120]}{'...' if len(text) > 120 else ''}",
90+
"href": f"https://x.com/{username}/status/{tweet_id}",
91+
"body": f"{text}\n\n[{engagement}]",
92+
})
93+
94+
return search_results

0 commit comments

Comments
 (0)