3a3a0101cf
Based on x-reader by @runes_leo (MIT License). Extended with: - Reddit support (posts + comments, proxy support) - GitHub support (repos, issues, PRs) - Web search via Exa semantic search - Reddit search (bypasses IP blocks via Exa) - GitHub search (repos by stars) - Renamed package: x_reader → agent_eyes - New MCP tools: search, search_reddit, search_github - Agent-first positioning and documentation
95 lines
2.4 KiB
Python
95 lines
2.4 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Search fetcher — semantic web search via Exa API.
|
|
|
|
Requires EXA_API_KEY env var. Get a free key at https://exa.ai
|
|
"""
|
|
|
|
import os
|
|
import requests
|
|
from loguru import logger
|
|
from typing import Dict, Any, List, Optional
|
|
|
|
|
|
EXA_API_URL = "https://api.exa.ai/search"
|
|
|
|
|
|
async def search_web(
|
|
query: str,
|
|
num_results: int = 5,
|
|
search_type: str = "auto",
|
|
) -> List[Dict[str, Any]]:
|
|
"""
|
|
Search the web using Exa semantic search.
|
|
|
|
Args:
|
|
query: Search query (supports site: prefix, e.g. "site:reddit.com AI agent")
|
|
num_results: Number of results to return (default 5, max 10)
|
|
search_type: "auto" (default) or "neural" or "keyword"
|
|
|
|
Returns:
|
|
List of search results with title, url, snippet
|
|
"""
|
|
api_key = os.environ.get("EXA_API_KEY")
|
|
if not api_key:
|
|
raise ValueError(
|
|
"EXA_API_KEY not set. Get a free key at https://exa.ai\n"
|
|
"Then: export EXA_API_KEY=your_key_here"
|
|
)
|
|
|
|
logger.info(f"Exa search: {query} (n={num_results})")
|
|
|
|
resp = requests.post(
|
|
EXA_API_URL,
|
|
headers={
|
|
"Content-Type": "application/json",
|
|
"x-api-key": api_key,
|
|
},
|
|
json={
|
|
"query": query,
|
|
"numResults": min(num_results, 10),
|
|
"type": search_type,
|
|
"contents": {
|
|
"text": {"maxCharacters": 500},
|
|
},
|
|
},
|
|
timeout=15,
|
|
)
|
|
resp.raise_for_status()
|
|
data = resp.json()
|
|
|
|
results = []
|
|
for item in data.get("results", []):
|
|
results.append({
|
|
"title": item.get("title", ""),
|
|
"url": item.get("url", ""),
|
|
"snippet": item.get("text", ""),
|
|
"published_date": item.get("publishedDate", ""),
|
|
"score": item.get("score", 0),
|
|
})
|
|
|
|
return results
|
|
|
|
|
|
async def search_reddit_via_exa(
|
|
query: str,
|
|
subreddit: Optional[str] = None,
|
|
num_results: int = 10,
|
|
) -> List[Dict[str, Any]]:
|
|
"""
|
|
Search Reddit content via Exa (bypasses Reddit IP blocks).
|
|
|
|
Args:
|
|
query: Search query
|
|
subreddit: Optional subreddit to limit search (e.g. "LocalLLaMA")
|
|
num_results: Number of results
|
|
|
|
Returns:
|
|
List of Reddit posts found
|
|
"""
|
|
if subreddit:
|
|
full_query = f"site:reddit.com/r/{subreddit} {query}"
|
|
else:
|
|
full_query = f"site:reddit.com {query}"
|
|
|
|
return await search_web(full_query, num_results=num_results)
|